污点分析挖掘漏洞演示——如何在8小时内从零发现cve2012-0158(word溢出漏洞)
简介
最近一直在研究漏洞挖掘的东西。
网上能看到的漏洞挖掘的方法,大概就两种,一种是fuzzing,一种是静态分析和动态调试结合,人工审查。
我比较能接受后者,关于这种方法还写过一个帖子,分析网络蚂蚁的栈溢出的。
但是这种方法有一个很大的缺点,当你分析的程序大到一定级别的时候,会变得非常非常非常困难。比如分析word之类的软件....
那么要怎么解决呢?如果你看过我分析网络蚂蚁那个帖子,应该就会发现,我的分析思路,基本上就是人工跟踪数据流,然后看数据流的路径上,有没有异常。
word的漏洞之所以难挖掘,难分析,很大程度就是因为数据流太难跟踪了。那么能不能把这个过程自动化呢?答案是能,利用的就是下面要说的“污点分析”方法。
污点分析简介
我们都知道cpu运行程序的过程,就是按照顺序执行一系列的指令。
指令有操作数,分为源操作数和目的操作数。
操作数又分为内存和寄存器两种。
如果我们把某个源操作数标记为污点数据,那么这条指令执行后,目的操作数也变成了污点数据,这就是污点的传播。
比如下面这个简单的x86汇编程序
1. mov eax, [ebx]
2. mov ecx, eax
3. mov ebx, 0
4. mov edx, ebx
如果ebx对应的内存地址上的数据被标记为污点数据,那么执行完1后,eax也会被标记为污点数据;再执行完2后,ecx被标记为污点数据。
如果我们能把程序执行的指令,都记录下来(比如利用dynamorio之类),然后一条一条解释传播,把源操作数为污点数据的指令记录起来,就可以得到受污点数据影响的程序路径。
比如上面的汇编片段,按照之前的假设,得到的污点路径是1、2。而3、4不受污点数据影响。
分析得到污点路径这个过程就叫污点分析。
溢出漏洞模型
漏洞就是某种特殊的代码执行路径,比如下面这种经典的栈溢出代码
int taintCnt = ...
char stackBuffer[100];
memcpy(stackBuffer, src, taitnCnt);
显然,如果我们能在受污点数据影响的路径上,发现类似的路径片段,那么几乎就可以认为这部分代码存在溢出漏洞。
但是上面的代码还不够抽象,要提取出一个抽象特征来,才可以在污点路径上去匹配。
对于上面的代码,特征就是我们可以用污点数据控制复制的次数,更本质地说,是循环的次数。
转换成x86汇编,可能就是如下形式
rep movsd, ecx (ecx受控制)
或者
CopyLoop:
mov eax, [esi]
mov [edi], eax
inc esi
inc edi
inc ecx
cmp ecx, taintCnt
jnz CopyLoop
离线调试器
要完全自动分析,准确确定某个地方是不是有漏洞,非常困难;但是如果把有问题的地方标记出来,然后再人工确认,准确率就可以轻松做到很高。
所以要把采集到的指令数据利用起来——在ida中同步显示,也就是离线调试器。
利用离线数据可以很轻松地开启上帝视角,分析起来非常安逸。
下图是ida中的离线调试器客户端界面。(黄色标记代表受污点影响的指令,蓝色标记代表当前指令)
ida中的客户端是用ida的py脚本+Qt实现的。
下图是离线调试器的服务端,ida中的客户端从这里拿数据(http接口)。
演示开始——利用word生成正常的rtf样本文件
假设时间回到2012年,我们想分析一下word2007中,解析listview控件的时候,有没有漏洞。
从“开发工具”里面,添加listview control。
因为是分析解析过程有没有漏洞,所以往listview里面添加一些数据
保存为启用宏的文档(*.docm)
再打开那个docm文件,另存为rtf文件
这时候有太多不是我们想分析的数据,把它们都删掉,具体地将是只保留object标记里面的数据。
精简之后的原始样本大小大概是19,149 字节。
采集数据
通过简单的调试(或者利用污点分析也可以),可以知道word读取rtf的地方在wwlib.dll的这个地方
所以我们利用某个类似条件断点的工具,让dynamorio在执行到3126FC0A处后,再开始记录数据。
先记录6千万条试试看。
记录完成大概得到4.07 GB的指令数据。每条指令记录包含的数据有:对应的eip,8个通用寄存器,8个4字节的栈记录。
分析开始
从离线数据中,找出调用ReadFile的地方
ReadFile的参数——缓冲区地址,就是污点源。
从call之后返回的第一条指令开始传播。
比如图中的3126FC2D。
分析1
污点分析结果告诉我们,在偏移为0xCA处,有数据可以控制复制的字节数。
此处的数据如下图
我们去离线调试器里看看。 rep mov来自msvcr80,往上一层,来到wwlib,可以看到是调用了memmove函数。
可以看到edi的值是0x2200,正好和0xCA附近的值对应起来,所以应该是0xCA附近的几个字节控制复制次数。
dst buffer(push esi)不是栈上缓冲区,所以我们看看它是在哪里分配的,有没有可能溢出。
利用离线调试器的"go to prev call"功能,一层一层往上跳转,可以看到在ole32.dll里面分配内存。
调用GlobalAlloc进行分配,分配的大小和复制的大小一致,所以没有问题。
此外还有一些类似的点,就不全部一一列出来了。
然后下面进入真正的主题。
分析2
一步一步查看,过滤掉没有问题的地方后,我们可以跟到这里。
在离线调试器里,我们可以看到,dst buffer是栈上的缓冲区
控制复制次数的ecx来自2B6A偏移处
同时ecx需要满足:和某个值相等
.text:275C878A mov edi, [ebp+dwBytes]
.text:275C878D cmp [ebp+var_4], edi
.text:275C8790 jnz loc_275D3F93
从污点路径中可以发现,[ebp+var_4]来自2B72偏移处(相邻几个字节)。
再往上看看ecx还有没有其他的约束条件。
可以看到,ecx还需要大于或等于8。
同时dst buffer来自
.text:275C8A00 lea eax, [ebp+var_8]
也就是只有8字节。
到这里,这部分代码就满足上面的漏洞模型
1. 往栈上缓冲区写入数据
2. 写入大小可控,而且可以超过缓冲区大小(大于或等于8)
把rtf中那两处偏移上的08字符串改成大于08的值,且保持相等,就可以实现栈缓冲区溢出,覆盖返回地址。
本文由看雪论坛 ktkitty 原创
转载请注明来自看雪论坛
往期热门阅读:
扫描二维码关注我们,更多干货等你来拿!